/**
 * OSS Upload SDK
 * A simple SDK for uploading files to OSS
 */

const OSS = require('ali-oss');

class OssUploadClient {
  /**
   * Create a new OSS client
   * @param {Object} options - Configuration options
   * @param {string} options.accessKeyId - Access key ID
   * @param {string} options.accessKeySecret - Access key secret
   * @param {string} options.bucket - Bucket name
   * @param {string} options.region - Region
   * @param {string} options.stsToken - STS token (optional)
   * @param {string} options.baseDir - Base directory for uploads (optional)
   * @param {boolean} options.secure - Whether to use HTTPS (default: true)
   * @param {boolean} options.authorizationV4 - Whether to use V4 authorization (default: false)
   */
  constructor(options = {}) {
    this.options = {
      accessKeyId: options.accessKeyId,
      accessKeySecret: options.accessKeySecret,
      bucket: options.bucket,
      region: options.region,
      stsToken: options.stsToken,
      secure: options.secure !== undefined ? options.secure : true,
      authorizationV4: options.authorizationV4 || false,
      baseDir: options.baseDir || ''
    };

    this.client = new OSS(this.options);
  }

  /**
   * Get MIME type based on file extension
   * @param {string} fileName - Name of the file
   * @returns {string} MIME type
   */
  getFileMimeType(fileName) {
    const extension = fileName.toLowerCase().split('.').pop();
    const mimeTypes = {
      svg: 'image/svg+xml',
      png: 'image/png',
      jpg: 'image/jpeg',
      jpeg: 'image/jpeg',
      gif: 'image/gif',
    };
    return mimeTypes[extension] || '';
  }

  /**
   * Upload a file to OSS with multipart upload support
   * @param {File|Blob} file - File or Blob object to upload
   * @param {Object} options - Upload options
   * @param {Function} options.progress - Progress callback function
   * @param {number} options.parallel - Number of concurrent uploads
   * @param {number} options.partSize - Size of each part in bytes
   * @param {boolean} options.forbidOverwrite - Whether to forbid overwriting existing files
   * @returns {Promise<Object>} Upload result
   */
  async uploadFile(file, options = {}) {
    try {
      const fileNameSplit = file.name.split('.');
      const suffix = fileNameSplit.pop();
      const name = `${this.options.baseDir}/${fileNameSplit.join('')}${Date.now()}.${suffix}`;
      
      // Encode filename to handle special characters
      const encodedFileName = encodeURIComponent(file.name).replace(/['()]/g, escape);
      const mimeType = this.getFileMimeType(file.name);
      
      const headers = {
        // Specify cache behavior when the Object is downloaded.
        'Cache-Control': 'no-cache',
        // Specify the name of the Object when downloaded, using UTF-8 encoding to avoid issues with non-ASCII characters
        'Content-Disposition': `attachment; filename="${encodedFileName}"`,
        // Specify expiration time in milliseconds
        'Expires': '1000',
        // Specify storage type of the Object
        'x-oss-storage-class': 'Standard',
        // Specify Object tags, multiple tags can be set simultaneously
        'x-oss-tagging': 'Tag1=1&Tag2=2',
        // Specify whether to overwrite the same Object during multipart upload initialization
        'x-oss-forbid-overwrite': options.forbidOverwrite ? 'true' : 'false',
        'x-oss-object-acl': 'public-read', // File permission settings
        // Support cross-origin access by exposing header information
        'Access-Control-Expose-Headers': 'ETag,x-oss-request-id',
        ...(mimeType && { 'Content-Type': mimeType }),
      };

      const uploadOptions = {
        // Progress callback
        progress: options.progress,
        // Set the number of concurrent parts
        parallel: options.parallel || 4,
        // Set the part size. Default is 1 MB, minimum is 100 KB, maximum is 5 GB
        partSize: options.partSize || 1024 * 1024,
        headers,
        // Custom metadata
        meta: { year: new Date().getFullYear(), people: 'sdk-user' },
      };

      // Perform multipart upload
      const res = await this.client.multipartUpload(name, file, uploadOptions);
      
      return {
        name: name,
        url: `https://${this.options.bucket}.${this.options.region}.aliyuncs.com/${name}`,
        res: res
      };
    } catch (err) {
      // Handle request header errors caused by filename encoding
      if (err.name === 'TypeError' && err.message && err.message.includes('setRequestHeader')) {
        throw new Error('文件名包含特殊字符，请重命名后重新上传'); // Filename contains special characters, please rename and upload again
      } else {
        throw new Error(`上传失败: ${err.message}`); // Upload failed
      }
    }
  }

  /**
   * Save a file from URL to local download
   * @param {string} url - URL of the file to download
   * @param {string} fileName - Name to save the file as
   * @returns {Promise<void>}
   */
  async saveFile(url, fileName) {
    // This implementation is meant for browser environments
    // For Node.js environments, a different implementation would be needed
    if (typeof window === 'undefined') {
      throw new Error('saveFile is intended for browser environments');
    }

    try {
      const res = await fetch(url);
      const blob = await res.blob();
      const urlNew = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = urlNew;
      a.download = fileName;
      document.body.appendChild(a);
      a.click();
      a.remove();
      URL.revokeObjectURL(urlNew);
    } catch (err) {
      throw new Error(`Failed to save file: ${err.message}`);
    }
  }

  /**
   * Delete a file from OSS
   * @param {string} objectName - Object name in OSS
   * @returns {Promise<boolean>} Deletion result
   */
  async deleteFile(objectName) {
    try {
      await this.client.delete(objectName);
      return true;
    } catch (err) {
      console.error('Delete file error:', err);
      return false;
    }
  }
  
  /**
   * Get STS credentials and update the client
   * @param {Function} getCredentials - Function that returns a promise resolving to STS credentials
   * @returns {Promise<void>}
   */
  async updateCredentials(getCredentials) {
    try {
      const stsCredentials = await getCredentials();
      const newOptions = {
        region: `oss-${stsCredentials.regionId}`,
        accessKeyId: stsCredentials.accessKeyId,
        accessKeySecret: stsCredentials.accessKeySecret,
        stsToken: stsCredentials.securityToken,
        bucket: stsCredentials.bucketName,
        secure: true,
        authorizationV4: false,
        baseDir: stsCredentials.baseDir
      };
      
      this.client = new OSS(newOptions);
      this.options = {
        ...this.options,
        ...newOptions
      };
    } catch (err) {
      throw new Error(`Failed to update credentials: ${err.message}`);
    }
  }
  
  /**
   * Create an instance with automatic credential fetching
   * @param {Function} getCredentials - Function that returns a promise resolving to STS credentials
   * @returns {Promise<OssUploadClient>} New OssUploadClient instance
   */
  static async createWithCredentials(getCredentials) {
    try {
      const stsCredentials = await getCredentials();
      const options = {
        region: `oss-${stsCredentials.regionId}`,
        accessKeyId: stsCredentials.accessKeyId,
        accessKeySecret: stsCredentials.accessKeySecret,
        stsToken: stsCredentials.securityToken,
        bucket: stsCredentials.bucketName,
        secure: true,
        authorizationV4: false,
        baseDir: stsCredentials.baseDir
      };
      
      return new OssUploadClient(options);
    } catch (err) {
      throw new Error(`Failed to create client with credentials: ${err.message}`);
    }
  }
}

module.exports = OssUploadClient;